import {readFile, writeFile, mkdir} from 'node:fs/promises';
import {existsSync} from 'node:fs';
import {join} from 'node:path';

const saidsHW = 256;
const saidsLW = 128;
const saidsFile = 'saids.json';

let saids = new Map(); // key: room name, value: array of [userName, content]
const saidsMeta = (() => {
  const now = performance.now();
  return {
    lastModified: now,
    lastSaved: now,
  };
})();

const eventConnection = 'connection';
const eventDisconnect = 'disconnect';
const eventJoin = 'join';
const eventLoginRequired = 'loginrequired';
const eventSubmitLogin = 'submitlogin';
const eventLoginSuccess = 'loginsuccess';
const eventLoginFailed = 'loginfailed';
const eventSubmitSay = 'submitsay';
const eventSaids = 'saids';
const eventSays = 'says';

const multiSlashes = /\/{2,}/g;

async function loadSaids(syncDir) {
  const saidsPath = join(syncDir, saidsFile);
  if (!existsSync(saidsPath)) return;

  try {
    const saidsJson = await readFile(saidsPath, 'utf8');
    const saidsEntries = JSON.parse(saidsJson);
    saids = new Map(saidsEntries);
  } catch (err) {
    console.error(err);
  }
}

async function saveSaids(syncDir) {
  if (saidsMeta.lastModified <= saidsMeta.lastSaved) return;
  saidsMeta.lastSaved = saidsMeta.lastModified;

  await mkdir(syncDir, {recursive: true});
  const saidsEntries = Array.from(saids.entries());
  const saidsJson = JSON.stringify(saidsEntries);

  const saidsPath = join(syncDir, saidsFile);
  await writeFile(saidsPath, saidsJson);
}

function startIntervalSync(syncDir, syncInterval) {
  const syncTimer = setInterval(() => {
    saveSaids(syncDir);
  }, syncInterval);

  return function stopIntervalSync() {
    clearInterval(syncTimer);
  };
}

function normalizeRoomName(roomName) {
  roomName = roomName.replace(multiSlashes, '/');
  if (roomName.length > 1 && roomName.endsWith('/')) {
    roomName = roomName.substring(0, roomName.length - 1);
  }
  if (roomName) {
    roomName = roomName.toLocaleLowerCase();
  } else {
    roomName = '/';
  }
  return roomName;
}

function createChat(io, syncDir, syncInterval) {
  loadSaids(syncDir);
  const stopIntervalSync = startIntervalSync(syncDir, syncInterval);

  io.on(eventConnection, socket => {
    let data = {
      userName: '',
      roomName: '',
      roomIO: null,
      roomSaids: null,
    };

    function verifyLogin() {
      if (!data?.userName || !data?.roomName) {
        socket.emit(eventLoginRequired);
        return false;
      }
      return true;
    }

    function publishSays(contents) {
      const {userName, roomIO, roomSaids} = data;
      const says = contents.filter(Boolean).map(content => [userName, content]);

      roomIO.emit(eventSays, says);

      roomSaids.push(...says);
      const totalSaids = roomSaids.length + says.length;
      if (totalSaids > saidsHW) {
        const removeSaids = Math.max(0, totalSaids - saidsLW);
        roomSaids.splice(0, removeSaids);
      }
      saidsMeta.lastModified = performance.now();
    }

    socket.on(eventJoin, ({roomName}) => {
      data.roomName = normalizeRoomName(roomName);
      data.roomIO = io.to(roomName);

      let roomSaids;
      roomSaids = saids.get(roomName);
      if (roomSaids) {
        socket.emit(eventSaids, roomSaids);
      } else {
        roomSaids = [];
        saids.set(roomName, roomSaids);
      }
      data.roomSaids = roomSaids;

      socket.join(roomName);
    });

    socket.on(eventSubmitLogin, ({userName}) => {
      if (!userName) {
        socket.emit(eventLoginFailed);
        return;
      }

      data.userName = userName;
      socket.emit(eventLoginSuccess);
    });

    socket.on(eventDisconnect, () => {
      data = null;
    });

    socket.on(eventSubmitSay, content => {
      if (!content) return;
      if (!verifyLogin()) return;
      publishSays([content]);
    });
  });

  return async function onExit() {
    stopIntervalSync();
    await saveSaids(syncDir);
  };
}

export default createChat;
